/*
 * Copyright (C) 2014 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.nexd.app.okhttp;

import java.io.IOException;
import java.net.ProtocolException;

import cn.nexd.app.okhttp.internal.NamedRunnable;
import cn.nexd.app.okhttp.internal.http.HttpEngine;
import cn.nexd.app.okhttp.internal.http.RequestException;
import cn.nexd.app.okhttp.internal.http.RouteException;
import cn.nexd.app.okhttp.internal.http.StreamAllocation;


final class RealCall implements Call {
    private final OkHttpClient client;

    // Guarded by this.
    private boolean executed;
    volatile boolean canceled;

    /**
     * The application's original request unadulterated by redirects or auth headers.
     */
    Request originalRequest;
    HttpEngine engine;

    protected RealCall(OkHttpClient client, Request originalRequest) {
        this.client = client;
        this.originalRequest = originalRequest;
    }

    @Override
    public Request request() {
        return originalRequest;
    }

    @Override
    public Response execute() throws IOException {
        synchronized (this) {
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
        }
        try {
            client.dispatcher().executed(this);
            Response result = getResponseWithInterceptorChain(false);
            if (result == null) throw new IOException("Canceled");
            return result;
        } finally {
            client.dispatcher().finished(this);
        }
    }

    Object tag() {
        return originalRequest.tag();
    }

    @Override
    public void enqueue(Callback responseCallback) {
        enqueue(responseCallback, false);
    }

    void enqueue(Callback responseCallback, boolean forWebSocket) {
        synchronized (this) {
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
        }
        client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
    }

    @Override
    public void cancel() {
        canceled = true;
        if (engine != null) engine.cancel();
    }

    @Override
    public synchronized boolean isExecuted() {
        return executed;
    }

    @Override
    public boolean isCanceled() {
        return canceled;
    }

    final class AsyncCall extends NamedRunnable {
        private final Callback responseCallback;
        private final boolean forWebSocket;

        private AsyncCall(Callback responseCallback, boolean forWebSocket) {
            super("OkHttp %s", originalRequest.url().toString());
            this.responseCallback = responseCallback;
            this.forWebSocket = forWebSocket;
        }

        String host() {
            return originalRequest.url().host();
        }

        Request request() {
            return originalRequest;
        }

        Object tag() {
            return originalRequest.tag();
        }

        void cancel() {
            RealCall.this.cancel();
        }

        RealCall get() {
            return RealCall.this;
        }

        @Override
        protected void execute() {
            boolean signalledCallback = false;
            try {
                Response response = getResponseWithInterceptorChain(forWebSocket);
                if (canceled) {
                    signalledCallback = true;
                    responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
                } else {
                    signalledCallback = true;
                    responseCallback.onResponse(RealCall.this, response);
                }
            } catch (IOException e) {
                if (signalledCallback) {
                    // Do not signal the callback twice!
                    //                    logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
                } else {
                    responseCallback.onFailure(RealCall.this, e);
                }
            } finally {
                client.dispatcher().finished(this);
            }
        }
    }

    /**
     * Returns a string that describes this call. Doesn't include a full URL as that might contain
     * sensitive information.
     */
    private String toLoggableString() {
        String string = canceled ? "canceled call" : "call";
        HttpUrl redactedUrl = originalRequest.url().resolve("/...");
        return string + " to " + redactedUrl;
    }

    private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
        Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
        return chain.proceed(originalRequest);
    }

    class ApplicationInterceptorChain implements Interceptor.Chain {
        private final int index;
        private final Request request;
        private final boolean forWebSocket;

        ApplicationInterceptorChain(int index, Request request, boolean forWebSocket) {
            this.index = index;
            this.request = request;
            this.forWebSocket = forWebSocket;
        }

        @Override
        public Connection connection() {
            return null;
        }

        @Override
        public Request request() {
            return request;
        }

        @Override
        public Response proceed(Request request) throws IOException {
            // If there's another interceptor in the chain, call that.
            if (index < client.interceptors().size()) {
                Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
                Interceptor interceptor = client.interceptors().get(index);
                Response interceptedResponse = interceptor.intercept(chain);

                if (interceptedResponse == null) {
                    throw new NullPointerException("application interceptor " + interceptor
                            + " returned null");
                }

                return interceptedResponse;
            }

            // No more interceptors. Do HTTP.
            return getResponse(request, forWebSocket);
        }
    }

    /**
     * Performs the request and returns the response. May return null if this call was canceled.
     */
    Response getResponse(Request request, boolean forWebSocket) throws IOException {
        // Copy body metadata to the appropriate request headers.
        RequestBody body = request.body();
        if (body != null) {
            Request.Builder requestBuilder = request.newBuilder();

            MediaType contentType = body.contentType();
            if (contentType != null) {
                requestBuilder.header("Content-Type", contentType.toString());
            }

            long contentLength = body.contentLength();
            if (contentLength != -1) {
                requestBuilder.header("Content-Length", Long.toString(contentLength));
                requestBuilder.removeHeader("Transfer-Encoding");
            } else {
                requestBuilder.header("Transfer-Encoding", "chunked");
                requestBuilder.removeHeader("Content-Length");
            }

            request = requestBuilder.build();
        }

        // Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
        engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);

        int followUpCount = 0;
        while (true) {
            if (canceled) {
                engine.releaseStreamAllocation();
                throw new IOException("Canceled");
            }

            boolean releaseConnection = true;
            try {
                engine.sendRequest();
                engine.readResponse();
                releaseConnection = false;
            } catch (RequestException e) {
                // The attempt to interpret the request failed. Give up.
                throw e.getCause();
            } catch (RouteException e) {
                // The attempt to connect via a route failed. The request will not have been sent.
                HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null);
                if (retryEngine != null) {
                    releaseConnection = false;
                    engine = retryEngine;
                    continue;
                }
                // Give up; recovery is not possible.
                throw e.getLastConnectException();
            } catch (IOException e) {
                // An attempt to communicate with a server failed. The request may have been sent.
                HttpEngine retryEngine = engine.recover(e, null);
                if (retryEngine != null) {
                    releaseConnection = false;
                    engine = retryEngine;
                    continue;
                }

                // Give up; recovery is not possible.
                throw e;
            } finally {
                // We're throwing an unchecked exception. Release any resources.
                if (releaseConnection) {
                    StreamAllocation streamAllocation = engine.close();
                    streamAllocation.release();
                }
            }

            Response response = engine.getResponse();
            Request followUp = engine.followUpRequest();

            if (followUp == null) {
                if (!forWebSocket) {
                    engine.releaseStreamAllocation();
                }
                return response;
            }

            StreamAllocation streamAllocation = engine.close();

            if (++followUpCount > HttpEngine.MAX_FOLLOW_UPS) {
                streamAllocation.release();
                throw new ProtocolException("Too many follow-up requests: " + followUpCount);
            }

            if (!engine.sameConnection(followUp.url())) {
                streamAllocation.release();
                streamAllocation = null;
            }

            request = followUp;
            engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
                    response);
        }
    }
}
